﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SecurityManager.cs" company="TechBits">
//   Copyright (c) TechBits. All rights reserved.
// </copyright>
// <summary>
//   Defines the SecurityManager type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace IntelliShortCut.Core
{
    using System;
    using System.Collections.Generic;
    using System.IO;    

    using System.Reflection;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Security.Cryptography;
    using System.Text;

    using Application;
    using Data;
    using log4net;
    
    /// <summary>
    /// Security manager.
    /// </summary>
    public class SecurityManager
    {
        #region "Private Members"

        /// <summary>
        /// Object used for locking purposes.
        /// </summary>
        private static readonly object locker = new object();

        /// <summary>
        /// Logger interface.
        /// </summary>
        private static readonly ILog _logger;

        #endregion

        #region "Constructors"

        /// <summary>
        /// Initializes static members of the <see cref="SecurityManager"/> class.
        /// </summary>
        static SecurityManager()
        {
            _logger = LogManager.GetLogger(typeof(SecurityManager));
        }

        #endregion

        #region "Public Methods"

        /// <summary>
        /// Generates a MD5 key for the object.
        /// </summary>
        /// <remarks>
        /// Source object must be serializable.
        /// </remarks>
        /// <param name="sourceObject">The source object.</param>
        /// <returns>A MD5 key.</returns>
        /// <exception cref="ArgumentNullException">If the source object is null.</exception>
        /// <exception cref="ApplicationException">If the source object is not serializable.</exception>
        public static string GenerateMD5Key(object sourceObject)
        {
            if (sourceObject == null)
            {
                throw new ArgumentNullException("sourceObject");
            }

            string hashString;

            // Determine if the passed object is really serializable
            try
            {
                hashString = ComputeHash(ObjectToByteArray(sourceObject));
            }
            catch (AmbiguousMatchException ame)
            {
                throw new ApplicationException("Could not definitly decide if object is serializable.", ame);
            }

            return hashString;
        }

        /// <summary>
        /// Provides a unique key string.
        /// </summary>
        /// <returns>
        /// Unique key string.
        /// </returns>
        public static string GetUniqueKey()
        {
            return RNGCharacterMask();
        }

        /// <summary>
        /// Checks the data integrity.
        /// </summary>
        /// <param name="shortcuts">The shortcuts.</param>
        /// <returns>
        /// True is the data integrity has been conserved; false otherwise.
        /// </returns>
        public static bool CheckDataIntegrity(List<ShortcutData> shortcuts)
        {           
            try
            {   
                // Checks if the security verification is disabled
                if (!ApplicationConfiguration.Settings.SecurityValidation)
                {
                    return true;
                }

                foreach (var shortcutData in shortcuts)
                {
                    // Prepares the shortcut for md5 checking
                    var shortcutMD5 = (ShortcutData)shortcutData.Clone();
                    shortcutMD5.MD5Key = null;

                    if (!shortcutData.MD5Key.Equals(GenerateMD5Key(shortcutMD5)))
                    {
                        _logger.ErrorFormat("Security Error: the MD5 key is invalid. Details: {0}", shortcutMD5);
                        return false;
                    }
                }                
            }
            catch (Exception)
            {                
                return false;
            }

            return true;
        }

        #endregion

        #region "Private Methods"

        /// <summary>
        /// Computes the MD5 hash function over the object represented as bytes.
        /// </summary>
        /// <param name="objectAsBytes">The object as bytes.</param>
        /// <returns>
        /// The result of the MD5 hash function.
        /// </returns>
        private static string ComputeHash(byte[] objectAsBytes)
        {
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] result = md5.ComputeHash(objectAsBytes);

            // Build the final string by converting each byte
            // into hex and appending it to a StringBuilder
            var sb = new StringBuilder();
            for (int i = 0; i < result.Length; i++)
            {
                sb.Append(result[i].ToString("X2"));
            }

            return sb.ToString();
        }

        /// <summary>
        /// Serializes an object to an array of bytes.
        /// </summary>
        /// <param name="objectToSerialize">The object to serialize.</param>
        /// <returns>
        /// The object serializaed as an array of bytes.
        /// </returns>
        private static byte[] ObjectToByteArray(object objectToSerialize)
        {
            using (var fs = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                lock (locker)
                {
                    formatter.Serialize(fs, objectToSerialize);
                }

                return fs.ToArray();
            }
        }

        /// <summary>
        /// Generates a unique key of fixed length.
        /// </summary>
        /// <returns>
        /// Unique key of fixed length.
        /// </returns>
        private static string RNGCharacterMask()
        {
            const int Size = 8;

            char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
            var data = new byte[1];

            var crypto = new RNGCryptoServiceProvider();
            crypto.GetNonZeroBytes(data);

            data = new byte[8];
            crypto.GetNonZeroBytes(data);

            var result = new StringBuilder(Size);
            foreach (byte b in data)
            {
                result.Append(chars[b % (chars.Length - 1)]);
            }

            return result.ToString();
        }

        #endregion
    }
}
